﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Cryptography;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using System.IO;

namespace ResourceBlender.Core {
    /// <summary>
    /// Miscellaneuous methods
    /// </summary>
    public delegate void ShowMessageDelegate(string message, string caption);
    public class Utils {
        private static Random seed = new Random();
        /// <summary>
        /// Joins a list using a separator.
        /// </summary>
        /// <param name="list">The list to join.</param>
        /// <param name="separator">The separator to use.</param>
        /// <param name="trim">If set to <c>true</c>, trims whitespace from each index.</param>
        /// <returns>A string containing the values separated by the separator</returns>
        public static string Implode(IEnumerable<string> list, string separator, bool trim) {
            return Implode(list, separator, trim, null);
        }

        /// <summary>
        /// Joins a list using a separator.
        /// </summary>
        /// <param name="list">The list to join.</param>
        /// <param name="separator">The separator to use.</param>
        /// <param name="trim">If set to <c>true</c>, trims whitespace from each index.</param>
        /// <param name="useAnd">The separator to use to join the second to last and last item, or <c>null</c> to use the separator. Eg: Could be "and".</param>
        /// <returns>A string containing the values separated by the separator</returns>
        public static string Implode(IEnumerable<string> list, string separator, bool trim, string lastSeparator) {
            StringBuilder buffer = new StringBuilder();
            int num = list.Count(),
                added = 0;
            foreach(string item in list) {
                if(trim) {
                    buffer.Append(item.Trim());
                } else {
                    buffer.Append(item);
                }
                if(added + 1 < num) {
                    if(added == num - 2 && lastSeparator != null) {
                        buffer.Append(lastSeparator);
                    } else {
                        buffer.Append(separator);
                    }
                }
                added++;
            }
            return buffer.ToString();
        }

        /// <summary>
        /// Generates a (hopefully) unique ID using a combination of 53 (non-HTML safe) or 39 (HTML safe) characters.
        /// There are 1.74e17 combinations if the ID does not need to be HTML safe, otherwise there are just over 3500 million
        /// </summary>
        /// <param name="HTMLSafe"><c>true</c> if the generated ID must be valid in HTML without encoding.</param>
        /// <returns>The generated ID</returns>
        public static string GenID(bool HTMLSafe) {
            StringBuilder bld = new StringBuilder();
            int len = 6;
            if(HTMLSafe) {
                string possible = "ABCEDFGHIJKLMNOPQRSTUVWXYZ0123456789~_-",
                       firstPossible = "ABCEDFGHIJKLMNOPQRSTUVWXYZ"; // make sure the first character isn't a number, which invalidates xhtml
                bld.Append(possible[seed.Next(firstPossible.Length - 1)]);
                for(int i = 1; i < len; i++) {
                    bld.Append(possible[seed.Next(possible.Length - 1)]);
                }
            } else {
                string possible = "ABCEDFGHIJKLMNOPQRSTUVWXYZ0123456789%£$!*/#~][{}|=_-^";
                for(int i = 0; i < len; i++) {
                    bld.Append(possible[seed.Next(possible.Length - 1)]);
                }
            }
            return bld.ToString();
        }

        /// <summary>
        /// Calculates the MD5 hash of the specified text
        /// </summary>
        /// <param name="text">The text.</param>
        /// <returns>The MD5 hash.</returns>
        public static string MD5(string text) {
            MD5 md5 = new MD5CryptoServiceProvider();
            byte[] originalBytes = ASCIIEncoding.UTF8.GetBytes(text);
            byte[] encodedBytes = md5.ComputeHash(originalBytes);
            return BitConverter.ToString(encodedBytes).Replace("-", "").ToLower();
        }

        /// <summary>
        /// Applies salt to a password.
        /// </summary>
        /// <param name="password">The password.</param>
        /// <returns></returns>
        public static string SaltPassword(string password) {
            return SaltPassword(password, CoreConfigurationManager.Salt);
        }

        /// <summary>
        /// Applies salt to a password.
        /// </summary>
        /// <param name="password">The password.</param>
        /// <param name="salt">The salt to use.</param>
        /// <returns></returns>
        public static string SaltPassword(string password, string salt) {
            return MD5(salt + password);
        }

        /// <summary>
        /// Converts a .NET compatible country code to a Java compatible country code. These are mostly the same apart from a couple of unique cases.
        /// </summary>
        /// <param name="dotNetCulture">The dot net culture.</param>
        /// <returns></returns>
        public static string DotNetCulture2JavaCulture(string dotNetCulture) {
            string javaCulture = string.Empty;
            switch(dotNetCulture) {
                case "he-IL":
                    javaCulture = "iw_IL"; // bug as of Java v1.6.7
                    break;
                case "zh-Hans":
                case "zh-CHS":
                    javaCulture = "zh_CN";
                    break;
                case "zh-Hant":
                case "zh-CHT":
                    javaCulture = "zh_TW";
                    break;
                default:
                    javaCulture = dotNetCulture.Replace('-', '_');
                    break;
            }
            return javaCulture;
        }

        /// <summary>
        /// Converts a Java compatible country code to a .NET compatible country code. These are mostly the same apart from a couple of unique cases.
        /// </summary>
        /// <param name="dotNetCulture">The dot net culture.</param>
        /// <returns></returns>
        public static string JavaCulture2DotNetCulture(string javaCulture) {
            string dotNetCulture = string.Empty;
            switch(javaCulture) {
                case "iw_IL":
                    dotNetCulture = "he-IL"; // bug as of Java v1.6.7
                    break;
                default:
                    dotNetCulture = javaCulture.Replace('_', '-');
                    break;
            }
            return dotNetCulture;
        }

        /// <summary>
        /// For the specified string, replaces each Regex in pattern[], with it's equivalent index of replacements[]
        /// </summary>
        /// <param name="input">The input.</param>
        /// <param name="pattern">The pattern.</param>
        /// <param name="replacements">The replacements.</param>
        /// <returns></returns>
        public static string PregReplace(string stringToReplace, string[] pattern, string[] replacements, RegexOptions options) {
            if(replacements.Length != pattern.Length) {
                throw new ArgumentException("Pattern and replacement arrays must be equal length");
            }
            for(int i = 0; i < pattern.Length; i++) {
                stringToReplace = Regex.Replace(stringToReplace, pattern[i], replacements[i], options);
            }
            return stringToReplace;
        }

        /// <summary>
        /// Replaces the template tags in the specified template with their values from the source and destination arrays provided.
        /// </summary>
        /// <param name="template">The template.</param>
        /// <param name="pattern">The array of template tags.</param>
        /// <param name="replacements">The array of replacements.</param>
        /// <returns>A formatted template tag</returns>
        public static string FormatTemplateTags(string template, string[] pattern, string[] replacements) {
            if(replacements.Length != pattern.Length) {
                throw new ArgumentException("Pattern and replacement arrays must be equal length");
            }
            if(!template.Contains("{=")) {
                return template;
            } else {
                StringBuilder buffer = new StringBuilder(template);
                for(int i = 0; i < pattern.Length; i++) {
                    buffer.Replace(pattern[i], replacements[i]);
                }
                return buffer.ToString();
            }
        }

        /// <summary>
        /// Trims a string to the specified length, adding an ellipsis (...) if it is longer than the required length.
        /// </summary>
        /// <param name="text">The text to trim.</param>
        /// <param name="length">The maximum length of the final string.</param>
        /// <returns>A trimmed version of the text with an ellipsis if it was too long</returns>
        public static string TrimText(string text, int length) {
            if(text == null) {
                return "";
            }
            string snippet = Regex.Replace(text, @"<(.|\n)*?>", string.Empty); // strip HTML tags
            if(snippet.Length > length) {
                int stringLen = length - 3;
                if(stringLen < 0) {
                    stringLen = 0;
                }
                return snippet.Substring(0, stringLen).Trim() + "...";
            } else {
                return snippet;
            }
        }

        /// <summary>
        /// Gets the CultureInfo for a given language code. If the culture does not exist, creates it for future use first.
        /// </summary>
        /// <param name="cultureCode">The culture name (code).</param>
        /// <returns>A CultureInfo representing the culture</returns>
        public static CultureInfo GetCultureInfo(string cultureCode) {
            if(cultureCode.Length < 2) {
                return null;
            }
            CultureInfo info;
            try {
                info = CultureInfo.GetCultureInfo(cultureCode);
            } catch {
                CultureAndRegionInfoBuilder builder = new CultureAndRegionInfoBuilder(cultureCode, CultureAndRegionModifiers.None);
                builder.LoadDataFromCultureInfo(CultureInfo.GetCultureInfo("en-GB"));
                builder.LoadDataFromRegionInfo(new RegionInfo("en-GB"));
                builder.TwoLetterISOLanguageName = cultureCode.Substring(0, 2);
                builder.TwoLetterISORegionName = "unknown";
                builder.CultureNativeName = cultureCode;
                builder.CultureEnglishName = cultureCode;
                builder.Register();
                info = CultureInfo.GetCultureInfo(cultureCode);
            }
            return info;
        }

        /// <summary>
        /// Gets the English name for the specified culture code, or returns the code if the culture is not available.
        /// </summary>
        /// <param name="cultureCode">The culture code.</param>
        /// <returns>The English name for the specified culture code, or returns the code if the culture is not available</returns>
        public static string GetSafeEnglishName(string cultureCode) {
            try {
                return CultureInfo.GetCultureInfo(cultureCode).EnglishName;
            } catch {
                return cultureCode;
            }
        }

        /// <summary>
        /// Gets the ISO 3166 code of the specified culture.
        /// </summary>
        /// <param name="info">The CultureInfo representing the culture.</param>
        /// <returns>An ISO3166 code to reresent a country which uses the CultureInfo specified</returns>
        public static string GetISO3166(CultureInfo info) {
            if(info.Name.Length == 0) {
                return null;
            } else if(info.IsNeutralCulture) {
                if(info.TwoLetterISOLanguageName == "zh") { // chinese is awkward
                    return "cn";
                } else {
                    return new RegionInfo(CultureInfo.CreateSpecificCulture(info.Name).LCID).TwoLetterISORegionName;
                }
            } else {
                return new RegionInfo(info.Name).TwoLetterISORegionName;
            }
        }

        /// <summary>
        /// Similar to Java's native2ascii utility.
        /// Replaces all characters with code points greater than 127 (0x007C) to unicode escape sequences (\uXXXX)
        /// </summary>
        /// <param name="text">The text.</param>
        /// <returns>The text with unicode escape sequences instead of codepoints.</returns>
        public static string UnicodeEscape(string text) {
            StringBuilder sb = new StringBuilder(text.Length*2);
            int highbits = 0;
            for(int i = 0; i < text.Length; i++) {
                char c = text[i];
                if(Constants.UnicodeEscapeRegex.IsMatch(c.ToString())) {
                    int num = (int)c;
                    if(num < 0xD800 || num > 0xDFFF) {
                        sb.Append("\\u" + Convert.ToString(num, 16).PadLeft(4, '0'));
                    } else if(num < 0xDC00) { // ... Surrogate high
                        highbits = num - 0xD800;
                    } else { // ... Surrogate low
                        sb.Append("\\u" + Convert.ToString(highbits << 10 + (num - 0xDC00) + 0x10000, 16).PadLeft(6, '0'));
                    }
                } else {
                    sb.Append(c);
                }
                /*if (num > 255) {
                    sb.Append("\\u");
                    ulong lowByte = ((ulong)num) >> 8;
                    //int lowByte = (num >> 8);
                    sb.Append(Convert.ToString((byte)lowByte, 16));
                    int highByte = (num & 0xFF);
                    sb.Append(Convert.ToString(highByte, 16));
                } else {
                        sb.Append(c);
                }
                continue;*/

                /*continue;
                 * fails with some eurpoean chars
                if(c <= 0x127) {
                    sb.Append(c);
                } else {
                    sb.Append("\\u" + Convert.ToString((int)c, 16).PadLeft(4,'0'));
                }*/
            }
            return sb.ToString();
        }

        /// <summary>
        /// Sanitizes a filename by removing invalid characters.
        /// </summary>
        /// <param name="path">The path.</param>
        /// <returns>The filename with invalid characters stripped.</returns>
        public static string SanitizeFilename(string path) {
            if(path == null || path.Length == 0) {
                return path;
            }
            string safePath = path;
            Array.ForEach(System.IO.Path.GetInvalidFileNameChars(), c => safePath = safePath.Replace(c.ToString(), string.Empty));
            return safePath;
        }

        /// <summary>
        /// Generates a path
        /// The parent directory path is an array of parent directories, the first valid directory is used as the directory name. If there are no valid directories,
        /// My Documents is used.
        /// This path is combined with the fileOrDirectoryName, or if the final path is invalid, with backupFileName.
        /// </summary>
        /// <param name="fileOrDirectoryName">Name of the file or directory.</param>
        /// <param name="backupFileName">Filename to use incase the first filename is invalid.</param>
        /// <param name="paths">The paths to be used as a base for the final path, in order of preference (the first valid one will be used).</param>
        /// <returns>A path containing the first  valid path and fileOrDirectoryName, or backupFileName if it is invalid.</returns>
        public static string GetNiceFilePath(string fileOrDirectoryName, string backupFileName, params string[] paths) {
            string basePath = string.Empty;
            foreach(string path in paths) {
                if(path.Length > 0) {
                    try {
                        if(Directory.Exists(path)) {
                            basePath = path;
                        } else {
                            FileInfo preferredPath = new FileInfo(path);
                            if(preferredPath.Extension.Length > 0) {
                                basePath = preferredPath.Directory.FullName;
                            } else {
                                //basePath = preferredPath.Directory.Parent.FullName; 
                                basePath = preferredPath.Directory.Parent.FullName;
                            }
                        }
                    } catch { }
                    if(basePath.Length > 0) { // found the path
                        break;
                    }
                }
            }
            if(basePath.Length == 0) {
                basePath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
            }
            string finalPath;
            try {
                finalPath = System.IO.Path.Combine(basePath, fileOrDirectoryName);
            } catch(ArgumentException) { // probably illegal characters in path
                finalPath = System.IO.Path.Combine(basePath, backupFileName);
            }
            return finalPath;
        }

        /// <summary>
        /// Retrieves the date the assembly was compiled.
        /// </summary>
        /// <returns></returns>
        public static DateTime RetrieveLinkerTimestamp() {
            string filePath = System.Reflection.Assembly.GetCallingAssembly().Location;
            const int c_PeHeaderOffset = 60;
            const int c_LinkerTimestampOffset = 8;
            byte[] b = new byte[2048];
            System.IO.Stream s = null;
            try {
                s = new System.IO.FileStream(filePath, System.IO.FileMode.Open, System.IO.FileAccess.Read);
                s.Read(b, 0, 2048);
            } finally {
                if(s != null) {
                    s.Close();
                }
            }
            int i = System.BitConverter.ToInt32(b, c_PeHeaderOffset);
            int secondsSince1970 = System.BitConverter.ToInt32(b, i + c_LinkerTimestampOffset);
            DateTime dt = new DateTime(1970, 1, 1, 0, 0, 0);
            dt = dt.AddSeconds(secondsSince1970);
            dt = dt.AddHours(TimeZone.CurrentTimeZone.GetUtcOffset(dt).Hours);
            return dt;
        }
    }
}
